/*
 * Created on Jun 20, 2003
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */
package org.gudy.azureus2.core3.config.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.gudy.azureus2.core3.config.COConfigurationListener;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.COConfigurationManager.ParameterVerifier;
import org.gudy.azureus2.core3.config.COConfigurationManager.ResetToDefaultsListener;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.config.PriorityParameterListener;
import org.gudy.azureus2.core3.config.StringList;
import org.gudy.azureus2.core3.util.AEDiagnostics;
import org.gudy.azureus2.core3.util.AEDiagnosticsEvidenceGenerator;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.BDecoder;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.ByteFormatter;
import org.gudy.azureus2.core3.util.ConcurrentHashMapWrapper;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.DelayedEvent;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.core3.util.FrequencyLimitedDispatcher;
import org.gudy.azureus2.core3.util.IndentWriter;
import org.gudy.azureus2.core3.util.SystemProperties;

import com.aelitis.azureus.core.security.CryptoManager;

/**
 * A singleton used to store configuration into a bencoded file.
 * 
 * @author TdC_VgA
 * 
 */
public class ConfigurationManager implements AEDiagnosticsEvidenceGenerator {
    private static final boolean DEBUG_PARAMETER_LISTENERS = false;

    private static ConfigurationManager config_temp = null;
    private static ConfigurationManager config = null;
    private static AEMonitor class_mon = new AEMonitor("ConfigMan:class");

    private ConcurrentHashMapWrapper<String, Object> propertiesMap; // leave this NULL - it picks up errors caused by initialisation sequence errors
    private List transient_properties = new ArrayList();

    private List<COConfigurationListener> listenerz = new ArrayList<COConfigurationListener>();
    private Map<String, ParameterListener[]> parameterListenerz = new HashMap<String, ParameterListener[]>();

    private List<ResetToDefaultsListener> reset_to_def_listeners = new ArrayList<ResetToDefaultsListener>();

    private static FrequencyLimitedDispatcher dirty_dispatcher = new FrequencyLimitedDispatcher(new AERunnable() {
        public void runSupport() {
            COConfigurationManager.save();
        }
    }, 30 * 1000);

    private ParameterListener exportable_parameter_listener = new ParameterListener() {

        public void parameterChanged(String key) {
            updateExportableParameter(key);
        }
    };

    private Map<String, String[]> exported_parameters = new HashMap<String, String[]>();
    private Map<String, String> imported_parameters = new HashMap<String, String>();
    private volatile boolean exported_parameters_dirty;

    public static ConfigurationManager getInstance() {
        try {
            class_mon.enter();

            if (config == null) {

                // this is nasty but I can't see an easy way around it. Unfortunately while reading the config
                // we hit other code (logging for example) that needs access to the config data. Things are
                // cunningly (?) arranged so that a recursive call here *won't* result in a further (looping)
                // recursive call if we attempt to load the config again. Hence this disgusting code that
                // goes for a second load attempt

                if (config_temp == null) {

                    config_temp = new ConfigurationManager();

                    config_temp.load();

                    config_temp.initialise();

                    config = config_temp;

                } else {

                    if (config_temp.propertiesMap == null) {

                        config_temp.load();
                    }

                    return (config_temp);
                }
            }

            return config;

        } finally {
            class_mon.exit();
        }
    }

    public static ConfigurationManager getInstance(Map data) {
        try {
            class_mon.enter();

            if (config == null) {

                config = new ConfigurationManager(data);
            }

            return config;
        } finally {

            class_mon.exit();
        }
    }

    private ConfigurationManager() {
    }

    private ConfigurationManager(Map data) {
        // default state of play for config initialised from map is debug log files off unless already
        // specified

        if (data.get("Logger.DebugFiles.Enabled") == null) {

            data.put("Logger.DebugFiles.Enabled", new Long(0));
        }

        propertiesMap = new ConcurrentHashMapWrapper<String, Object>(data);
    }

    protected void initialise() {

        // ConfigurationChecker.migrateConfig(); //removed 2201

        ConfigurationChecker.checkConfiguration();

        ConfigurationChecker.setSystemProperties();

        loadExportedParameters();

        AEDiagnostics.addEvidenceGenerator(this);
    }

    public void load(String filename) {
        Map data = FileUtil.readResilientConfigFile(filename, false);

        // horrendous recursive loading going on here due to logger + config depedencies. If already loaded
        // then use the existing data as it might have already been written to...

        if (propertiesMap == null) {

            ConcurrentHashMapWrapper<String, Object> c_map = new ConcurrentHashMapWrapper<String, Object>(data.size() + 256, 0.75f, 8);

            c_map.putAll(data);

            propertiesMap = c_map;
        }

        /*
         * Can't do this yet. Sometimes, there's a default set to x, but the code calls get..Parameter(..., y). y != x. When the user sets the the
         * parameter to x, we remove it from the list. Later, the get..Parameter(.., y) returns y because there is no entry. The solution is to not
         * allow get..Parameter(.., y) when there's a default value. Another reason to not allow it is that having two defaults confuses coders. //
         * Remove entries that are default. Saves memory, reduces // file size when saved again ConfigurationDefaults def =
         * ConfigurationDefaults.getInstance(); Iterator it = new TreeSet(propertiesMap.keySet()).iterator(); while (it.hasNext()) { String key =
         * (String)it.next(); Object defValue = def.getDefaultValueAsObject(key); if (defValue == null) continue; if (defValue instanceof Long) { int
         * iDefValue = ((Long)defValue).intValue(); int iValue = getIntParameter(key, iDefValue); if (iValue == iDefValue) propertiesMap.remove(key);
         * } if (defValue instanceof String) { String sDefValue = defValue.toString(); String sValue = getStringParameter(key, sDefValue); if
         * (sValue.compareTo(sDefValue) == 0) propertiesMap.remove(key); } }
         */
    }

    public void load() {
        load("azureus.config");

        try {
            String[] keys = propertiesMap.keySet().toArray(new String[0]);
            for (String key : keys) {
                if (key == null) {
                    continue;
                }
                if (key.startsWith("SideBar.Expanded.Category.") || key.startsWith("NameColumn.wrapText.")) {
                    removeParameter(key);
                }
            }
        } catch (Exception e) {
            // not sure if I can do Debug.out here.. could be in that evil
            // preinitialization loop of dooom
            e.printStackTrace();
        }
    }

    public void save(String filename) {
        if (propertiesMap == null) {

            // nothing to save, initialisation not complete

            return;
        }

        /**
         * Note - propertiesMap isn't synchronised! We'll clone the map now, because we need to modify it. The BEncoding code will create a new map
         * object (TreeMap) because it needs to be sorted, so we might as well do it here too.
         */
        TreeMap<String, Object> properties_clone = propertiesMap.toTreeMap();

        // Remove any transient parameters.
        if (!this.transient_properties.isEmpty()) {
            properties_clone.keySet().removeAll(this.transient_properties);
        }

        FileUtil.writeResilientConfigFile(filename, properties_clone);

        List<COConfigurationListener> listeners_copy;

        synchronized (listenerz) {

            listeners_copy = new ArrayList<COConfigurationListener>(listenerz);
        }

        for (int i = 0; i < listeners_copy.size(); i++) {

            COConfigurationListener l = (COConfigurationListener) listeners_copy.get(i);

            if (l != null) {

                try {
                    l.configurationSaved();

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            } else {

                Debug.out("COConfigurationListener is null");
            }
        }

        if (exported_parameters_dirty) {

            exportParameters();
        }
    }

    public void save() {
        save("azureus.config");
    }

    public void setDirty() {
        dirty_dispatcher.dispatch();
    }

    public boolean isNewInstall() {
        return (ConfigurationChecker.isNewInstall());
    }

    public Set<String> getDefinedParameters() {
        return (new HashSet<String>(propertiesMap.keySet()));
    }

    public boolean getBooleanParameter(String parameter, boolean defaultValue) {
        int defaultInt = defaultValue ? 1 : 0;
        int result = getIntParameter(parameter, defaultInt);
        return result == 0 ? false : true;
    }

    public boolean getBooleanParameter(String parameter) {
        ConfigurationDefaults def = ConfigurationDefaults.getInstance();
        int result;
        try {
            result = getIntParameter(parameter, def.getIntParameter(parameter));
        } catch (ConfigurationParameterNotFoundException e) {
            result = getIntParameter(parameter, ConfigurationDefaults.def_boolean);
        }
        return result == 0 ? false : true;
    }

    public boolean setParameter(String parameter, boolean value) {
        return setParameter(parameter, value ? 1 : 0);
    }

    private Long getLongParameterRaw(String parameter) {
        try {
            return (Long) propertiesMap.get(parameter);
        } catch (Exception e) {
            Debug.out("Parameter '" + parameter + "' has incorrect type", e);
            return null;
        }
    }

    public int getIntParameter(String parameter, int defaultValue) {
        Long tempValue = getLongParameterRaw(parameter);
        return tempValue != null ? tempValue.intValue() : defaultValue;
    }

    public int getIntParameter(String parameter) {
        ConfigurationDefaults def = ConfigurationDefaults.getInstance();
        int result;
        try {
            result = getIntParameter(parameter, def.getIntParameter(parameter));
        } catch (ConfigurationParameterNotFoundException e) {
            result = getIntParameter(parameter, ConfigurationDefaults.def_int);
        }
        return result;
    }

    public long getLongParameter(String parameter, long defaultValue) {
        Long tempValue = getLongParameterRaw(parameter);
        return tempValue != null ? tempValue.longValue() : defaultValue;
    }

    public long getLongParameter(String parameter) {
        ConfigurationDefaults def = ConfigurationDefaults.getInstance();
        long result;
        try {
            result = getLongParameter(parameter, def.getLongParameter(parameter));
        } catch (ConfigurationParameterNotFoundException e) {
            result = getLongParameter(parameter, ConfigurationDefaults.def_long);
        }
        return result;
    }

    private byte[] getByteParameterRaw(String parameter) {
        return (byte[]) propertiesMap.get(parameter);
    }

    public byte[] getByteParameter(String parameter) {
        ConfigurationDefaults def = ConfigurationDefaults.getInstance();
        byte[] result;
        try {
            result = getByteParameter(parameter, def.getByteParameter(parameter));
        } catch (ConfigurationParameterNotFoundException e) {
            result = getByteParameter(parameter, ConfigurationDefaults.def_bytes);
        }
        return result;
    }

    public byte[] getByteParameter(String parameter, byte[] defaultValue) {
        byte[] tempValue = getByteParameterRaw(parameter);
        return tempValue != null ? tempValue : defaultValue;
    }

    private String getStringParameter(String parameter, byte[] defaultValue) {
        byte[] bp = getByteParameter(parameter, defaultValue);
        if (bp == null) {
            bp = getByteParameter(parameter, null);
        }
        if (bp == null)
            return null;
        return bytesToString(bp);
    }

    public String getStringParameter(String parameter, String defaultValue) {
        String tempValue = getStringParameter(parameter, (byte[]) null);
        return tempValue != null ? tempValue : defaultValue;
    }

    public String getStringParameter(String parameter) {
        ConfigurationDefaults def = ConfigurationDefaults.getInstance();
        String result;
        try {
            result = getStringParameter(parameter, def.getStringParameter(parameter));
        } catch (ConfigurationParameterNotFoundException e) {
            result = getStringParameter(parameter, ConfigurationDefaults.def_String);
        }
        return result;
    }

    public StringList getStringListParameter(String parameter) {
        try {
            List rawList = (List) propertiesMap.get(parameter);
            if (rawList == null)
                return new StringListImpl();
            return new StringListImpl(rawList);
        } catch (Exception e) {
            Debug.out("Parameter '" + parameter + "' has incorrect type", e);
            return new StringListImpl();
        }
    }

    public boolean setParameter(String parameter, StringList value) {
        try {
            List encoded = new ArrayList();

            List l = ((StringListImpl) value).getList();

            for (int i = 0; i < l.size(); i++) {

                encoded.add(stringToBytes((String) l.get(i)));
            }
            propertiesMap.put(parameter, encoded);
            notifyParameterListeners(parameter);
        } catch (Exception e) {
            Debug.printStackTrace(e);
            return false;
        }
        return true;
    }

    public List getListParameter(String parameter, List def) {
        try {
            List rawList = (List) propertiesMap.get(parameter);
            if (rawList == null)
                return def;
            return rawList;
        } catch (Exception e) {
            Debug.out("Parameter '" + parameter + "' has incorrect type", e);
            return def;
        }
    }

    public boolean setParameter(String parameter, List value) {
        try {
            propertiesMap.put(parameter, value);
            notifyParameterListeners(parameter);
        } catch (Exception e) {
            Debug.printStackTrace(e);
            return false;
        }
        return true;
    }

    public Map getMapParameter(String parameter, Map def) {
        try {
            Map map = (Map) propertiesMap.get(parameter);
            if (map == null)
                return def;
            return map;
        } catch (Exception e) {
            Debug.out("Parameter '" + parameter + "' has incorrect type", e);
            return def;
        }
    }

    public boolean setParameter(String parameter, Map value) {
        try {
            propertiesMap.put(parameter, value);
            notifyParameterListeners(parameter);
        } catch (Exception e) {
            Debug.printStackTrace(e);
            return false;
        }
        return true;
    }

    public String getDirectoryParameter(String parameter) throws IOException {
        String dir = getStringParameter(parameter);

        if (dir.length() > 0) {
            File temp = new File(dir);
            if (!temp.exists()) {
                FileUtil.mkdirs(temp);
            }
            if (!temp.isDirectory()) {
                throw new IOException("Configuration error. This is not a directory: " + dir);
            }
        }

        return dir;
    }

    public float getFloatParameter(String parameter) {
        return (getFloatParameter(parameter, ConfigurationDefaults.def_float));
    }

    public float getFloatParameter(String parameter, float def_val) {
        ConfigurationDefaults def = ConfigurationDefaults.getInstance();
        try {
            Object o = propertiesMap.get(parameter);
            if (o instanceof Number) {
                return ((Number) o).floatValue();
            }

            String s = getStringParameter(parameter);

            if (!s.equals(ConfigurationDefaults.def_String))
                return Float.parseFloat(s);
        } catch (Exception e) {
            Debug.out("Parameter '" + parameter + "' has incorrect type", e);
        }

        try {
            return def.getFloatParameter(parameter);
        } catch (Exception e2) {
            return def_val;
        }
    }

    public boolean setParameter(String parameter, float defaultValue) {
        String newValue = String.valueOf(defaultValue);
        return setParameter(parameter, stringToBytes(newValue));
    }

    public boolean setParameter(String parameter, int defaultValue) {
        Long newValue = new Long(defaultValue);
        try {
            Long oldValue = (Long) propertiesMap.put(parameter, newValue);
            return notifyParameterListenersIfChanged(parameter, newValue, oldValue);
        } catch (ClassCastException e) {
            // Issuing a warning here would be nice, but both logging and config stuff
            // at startup create potential deadlocks or stack overflows
            notifyParameterListeners(parameter);
            return true;
        }
    }

    public boolean setParameter(String parameter, long defaultValue) {
        Long newValue = new Long(defaultValue);
        try {
            Long oldValue = (Long) propertiesMap.put(parameter, newValue);
            return notifyParameterListenersIfChanged(parameter, newValue, oldValue);
        } catch (ClassCastException e) {
            // Issuing a warning here would be nice, but both logging and config stuff
            // at startup create potential deadlocks or stack overflows
            notifyParameterListeners(parameter);
            return true;
        }
    }

    public boolean setParameter(String parameter, byte[] defaultValue) {
        try {
            byte[] oldValue = (byte[]) propertiesMap.put(parameter, defaultValue);
            return notifyParameterListenersIfChanged(parameter, defaultValue, oldValue);
        } catch (ClassCastException e) {
            // Issuing a warning here would be nice, but both logging and config stuff
            // at startup create potential deadlocks or stack overflows
            notifyParameterListeners(parameter);
            return true;
        }
    }

    public boolean setParameter(String parameter, String defaultValue) {
        return setParameter(parameter, stringToBytes(defaultValue));
    }

    /**
     * Returns true if a parameter with the given name exists.
     * 
     * @param key
     *            The name of the parameter to check.
     * @param explicit
     *            If <tt>true</tt>, we only check for a value which is definitely stored explicitly, <tt>false</tt> means that we'll also check
     *            against configuration defaults too.
     */
    public boolean hasParameter(String key, boolean explicit) {

        // We have an explicit value set.
        if (propertiesMap.containsKey(key)) {
            return true;
        }

        // We have a default value set.
        if ((!explicit) && ConfigurationDefaults.getInstance().hasParameter(key)) {
            return true;
        }

        return false;
    }

    public boolean verifyParameter(String parameter, String value) {
        List verifiers = ConfigurationDefaults.getInstance().getVerifiers(parameter);

        if (verifiers != null) {
            try {
                for (int i = 0; i < verifiers.size(); i++) {

                    ParameterVerifier verifier = (ParameterVerifier) verifiers.get(i);

                    if (verifier != null) {

                        try {
                            if (!verifier.verify(parameter, value)) {

                                return (false);
                            }
                        } catch (Throwable e) {

                            Debug.printStackTrace(e);
                        }
                    }
                }
            } catch (Throwable e) {

                // we're not synchronized so possible but unlikely error here

                Debug.printStackTrace(e);
            }
        }

        return (true);
    }

    public boolean setRGBParameter(String parameter, int red, int green, int blue) {
        boolean bAnyChanged = false;
        bAnyChanged |= setParameter(parameter + ".red", red);
        bAnyChanged |= setParameter(parameter + ".green", green);
        bAnyChanged |= setParameter(parameter + ".blue", blue);
        if (bAnyChanged)
            notifyParameterListeners(parameter);

        return bAnyChanged;
    }

    public boolean setRGBParameter(String parameter, int[] rgb, boolean override) {
        boolean changed = false;
        if (rgb == null) {
            changed |= removeParameter(parameter + ".override");
            changed |= removeParameter(parameter + ".red");
            changed |= removeParameter(parameter + ".green");
            changed |= removeParameter(parameter + ".blue");
        } else {
            changed |= setParameter(parameter + ".override", override);
            changed |= setRGBParameter(parameter, rgb[0], rgb[1], rgb[2]);
        }
        if (changed) {
            notifyParameterListeners(parameter);
        }
        return changed;
    }

    // Sets a parameter back to its default
    public boolean setParameter(String parameter) throws ConfigurationParameterNotFoundException {
        ConfigurationDefaults def = ConfigurationDefaults.getInstance();
        try {
            return setParameter(parameter, def.getIntParameter(parameter));
        } catch (Exception e) {
            return setParameter(parameter, def.getStringParameter(parameter));
        }
    }

    public Object getParameter(String name) {
        Object value = propertiesMap.get(name);

        if (value == null) {

            value = ConfigurationDefaults.getInstance().getParameter(name);
        }

        return (value);
    }

    /**
     * Set the raw parameter value to store in the properties map. This should only be used by trusted callers, and has been added to support external
     * plugin config files.
     * 
     * @param parameter
     *            Parameter name.
     * @param value
     *            A bencode-ably safe value.
     */
    public void setParameterRawNoNotify(String parameter, Object value) {
        this.propertiesMap.put(parameter, value);
    }

    /**
     * Use this method to record a parameter as one which can be stored here, but shouldn't be saved in azureus.config. Instead, some external object
     * should be responsible for the parameter's persistency (if it should have any at all).
     */
    public void registerTransientParameter(String param) {
        this.transient_properties.add(param);
    }

    /**
     * Remove the given configuration parameter completely.
     * 
     * @param parameter
     *            to remove
     * @return true if found and removed, false if not
     */
    public boolean removeParameter(String parameter) {
        boolean removed = propertiesMap.remove(parameter) != null;
        if (removed)
            notifyParameterListeners(parameter);
        return removed;
    }

    public boolean removeRGBParameter(String parameter) {
        boolean bAnyChanged = false;
        bAnyChanged |= removeParameter(parameter + ".red");
        bAnyChanged |= removeParameter(parameter + ".green");
        bAnyChanged |= removeParameter(parameter + ".blue");
        bAnyChanged |= removeParameter(parameter + ".override");
        if (bAnyChanged)
            notifyParameterListeners(parameter);

        return bAnyChanged;
    }

    /**
     * Does the given parameter exist.
     * 
     * @param parameter
     *            to check
     * @return true if exists, false if not present
     */

    public boolean doesParameterNonDefaultExist(String parameter) {
        return propertiesMap.containsKey(parameter);
    }

    private boolean notifyParameterListenersIfChanged(String parameter, Long newValue, Long oldValue) {
        if (oldValue == null || 0 != newValue.compareTo(oldValue)) {
            notifyParameterListeners(parameter);
            return true;
        }
        return false;
    }

    private boolean notifyParameterListenersIfChanged(String parameter, byte[] newValue, byte[] oldValue) {
        if (oldValue == null || !Arrays.equals(newValue, oldValue)) {
            notifyParameterListeners(parameter);
            return true;
        }
        return false;
    }

    public void addResetToDefaultsListener(ResetToDefaultsListener l) {
        synchronized (reset_to_def_listeners) {

            reset_to_def_listeners.add(l);
        }
    }

    public void registerExportedParameter(String name, String key) {
        synchronized (exported_parameters) {

            String[] entry = exported_parameters.get(key);

            if (entry == null) {

                entry = new String[] { name, imported_parameters.remove(name) };

                exported_parameters.put(key, entry);
            }
        }

        addParameterListener(key, exportable_parameter_listener);

        updateExportableParameter(key);
    }

    private void updateExportableParameter(String key) {
        Object o_value = getParameter(key);

        String value;

        if (o_value == null) {

            value = null;

        } else if (o_value instanceof byte[]) {

            try {
                value = new String((byte[]) o_value, "UTF-8");

            } catch (UnsupportedEncodingException e) {

                value = null;
            }
        } else {

            value = String.valueOf(o_value);
        }

        synchronized (exported_parameters) {

            String[] entry = exported_parameters.get(key);

            if (entry != null) {

                String existing = entry[1];

                if (existing != value) {

                    if (existing == null || value == null || !existing.equals(value)) {

                        entry[1] = value;

                        if (!exported_parameters_dirty) {

                            exported_parameters_dirty = true;

                            new DelayedEvent("epd", 5000, new AERunnable() {

                                @Override
                                public void runSupport() {
                                    exportParameters();
                                }
                            });
                        }
                    }
                }
            }
        }
    }

    private void exportParameters() {
        synchronized (exported_parameters) {

            if (!exported_parameters_dirty) {

                return;
            }

            exported_parameters_dirty = false;

            try {
                TreeMap<String, String> tm = new TreeMap<String, String>();

                Set<String> exported_keys = new HashSet<String>();

                for (String[] entry : exported_parameters.values()) {

                    String key = entry[0];
                    String value = entry[1];

                    exported_keys.add(key);

                    if (value != null) {

                        tm.put(key, value);
                    }
                }

                for (Map.Entry<String, String> entry : imported_parameters.entrySet()) {

                    String key = entry.getKey();

                    if (!exported_keys.contains(key)) {

                        tm.put(key, entry.getValue());
                    }
                }

                File parent_dir = new File(SystemProperties.getUserPath());

                File props = new File(parent_dir, "exported_params.properties");

                PrintWriter pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(props), "UTF-8"));

                try {
                    for (Map.Entry<String, String> entry : tm.entrySet()) {

                        pw.println(entry.getKey() + "=" + entry.getValue());
                    }

                } finally {

                    pw.close();
                }
            } catch (Throwable e) {

                e.printStackTrace();
            }
        }
    }

    private void loadExportedParameters() {
        synchronized (exported_parameters) {

            try {
                File parent_dir = new File(SystemProperties.getUserPath());

                File props = new File(parent_dir, "exported_params.properties");

                if (props.exists()) {

                    LineNumberReader lnr = new LineNumberReader(new InputStreamReader(new FileInputStream(props), "UTF-8"));

                    try {
                        while (true) {

                            String line = lnr.readLine();

                            if (line == null) {

                                break;
                            }

                            String[] bits = line.split("=");

                            if (bits.length == 2) {

                                String key = bits[0].trim();
                                String value = bits[1].trim();

                                if (key.length() > 0 && value.length() > 0) {

                                    imported_parameters.put(key, value);
                                }
                            }
                        }
                    } finally {

                        lnr.close();
                    }
                }
            } catch (Throwable e) {

                e.printStackTrace();
            }
        }

        COConfigurationManager.setIntDefault("instance.port", Constants.INSTANCE_PORT);

        registerExportedParameter("instance.port", "instance.port");
    }

    public void resetToDefaults() {
        ConfigurationDefaults def = ConfigurationDefaults.getInstance();

        List<String> def_names = new ArrayList<String>((Set<String>) def.getAllowedParameters());

        for (String s : def_names) {

            if (propertiesMap.remove(s) != null) {

                notifyParameterListeners(s);
            }
        }

        List<ResetToDefaultsListener> listeners;

        synchronized (reset_to_def_listeners) {

            listeners = new ArrayList<ResetToDefaultsListener>(reset_to_def_listeners);
        }

        for (ResetToDefaultsListener l : listeners) {

            try {
                l.reset();

            } catch (Throwable e) {

                Debug.out(e);
            }
        }

        save();
    }

    private void notifyParameterListeners(String parameter) {
        ParameterListener[] listeners;

        synchronized (parameterListenerz) {

            listeners = parameterListenerz.get(parameter);
        }

        if (listeners == null) {
            return;
        }

        for (ParameterListener listener : listeners) {

            if (listener != null) {

                try {
                    listener.parameterChanged(parameter);

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }
        }
    }

    public void addParameterListener(String parameter, ParameterListener new_listener) {
        if (parameter == null || new_listener == null) {

            return;
        }

        synchronized (parameterListenerz) {

            ParameterListener[] listeners = parameterListenerz.get(parameter);

            if (listeners == null) {

                parameterListenerz.put(parameter, new ParameterListener[] { new_listener });

            } else {

                ParameterListener[] new_listeners = new ParameterListener[listeners.length + 1];

                if (Constants.IS_CVS_VERSION && listeners.length > 100) {
                    Debug.out(parameter);
                }

                int pos;

                if (new_listener instanceof PriorityParameterListener) {

                    new_listeners[0] = new_listener;

                    pos = 1;

                } else {

                    new_listeners[listeners.length] = new_listener;

                    pos = 0;
                }

                for (int i = 0; i < listeners.length; i++) {

                    ParameterListener existing_listener = listeners[i];

                    if (existing_listener == new_listener) {

                        return;
                    }

                    new_listeners[pos++] = existing_listener;
                }

                if (DEBUG_PARAMETER_LISTENERS) {

                    System.out.println(parameter + "->" + new_listeners.length);
                }

                parameterListenerz.put(parameter, new_listeners);
            }
        }
    }

    public void removeParameterListener(String parameter, ParameterListener listener) {

        if (parameter == null || listener == null) {
            return;
        }

        synchronized (parameterListenerz) {
            ParameterListener[] listeners = parameterListenerz.get(parameter);

            if (listeners == null) {

                return;
            }

            if (listeners.length == 1) {

                if (listeners[0] == listener) {

                    parameterListenerz.remove(parameter);
                }
            } else {

                ParameterListener[] new_listeners = new ParameterListener[listeners.length - 1];

                int pos = 0;

                for (int i = 0; i < listeners.length; i++) {

                    ParameterListener existing_listener = listeners[i];

                    if (existing_listener != listener) {

                        if (pos == new_listeners.length) {

                            return;
                        }

                        new_listeners[pos++] = existing_listener;
                    }
                }

                if (DEBUG_PARAMETER_LISTENERS) {

                    System.out.println(parameter + "->" + new_listeners.length);
                }

                parameterListenerz.put(parameter, new_listeners);
            }
        }
    }

    public void addListener(COConfigurationListener listener) {
        synchronized (listenerz) {

            listenerz.add(listener);

        }
    }

    public void addAndFireListener(COConfigurationListener listener) {
        synchronized (listenerz) {

            listenerz.add(listener);

        }

        try {
            listener.configurationSaved();

        } catch (Throwable e) {

            Debug.printStackTrace(e);
        }
    }

    public void removeListener(COConfigurationListener listener) {
        synchronized (listenerz) {

            listenerz.remove(listener);
        }
    }

    private boolean ignoreKeyForDump(String key) {
        String lc_key = key.toLowerCase(Locale.US);

        if (key.startsWith(CryptoManager.CRYPTO_CONFIG_PREFIX) || lc_key.equals("id") || lc_key.endsWith(".privx") || lc_key.endsWith(".user")
                || lc_key.contains("password") || lc_key.contains("username") || lc_key.contains("session key")) {

            return (true);
        }

        Object value = propertiesMap.get(key);

        if (value instanceof byte[]) {

            try {
                value = new String((byte[]) value, "UTF-8");

            } catch (Throwable e) {

            }
        }

        if (value instanceof String) {

            if (((String) value).toLowerCase(Locale.US).endsWith(".b32.i2p")) {

                return (true);
            }
        }

        return (false);
    }

    public void generate(IndentWriter writer) {
        writer.println("Configuration Details");

        try {
            writer.indent();

            writer.println("version=" + Constants.AZUREUS_VERSION + ", subver=" + Constants.AZUREUS_SUBVER);

            writer.println("System Properties");

            try {
                writer.indent();

                Properties props = System.getProperties();

                Iterator it = new TreeSet(props.keySet()).iterator();

                while (it.hasNext()) {

                    String key = (String) it.next();

                    writer.println(key + "=" + props.get(key));
                }
            } finally {

                writer.exdent();
            }

            writer.println("Environment");

            try {
                writer.indent();

                Map<String, String> env = System.getenv();

                if (env == null) {

                    writer.println("Not supported");

                } else {

                    Iterator it = new TreeSet(env.keySet()).iterator();

                    while (it.hasNext()) {

                        String key = (String) it.next();

                        writer.println(key + "=" + env.get(key));
                    }
                }
            } finally {

                writer.exdent();
            }

            writer.println("Azureus Config");

            ConfigurationDefaults defaults = ConfigurationDefaults.getInstance();

            try {
                writer.indent();

                Set<String> keys = new TreeSet<String>(new Comparator<String>() {
                    public int compare(String o1, String o2) {
                        return (o1.compareToIgnoreCase(o2));
                    }
                });

                keys.addAll(propertiesMap.keySet());

                Iterator<String> it = keys.iterator();

                while (it.hasNext()) {

                    String key = it.next();

                    // don't dump crypto stuff

                    if (ignoreKeyForDump(key)) {

                        continue;
                    }

                    Object value = propertiesMap.get(key);

                    boolean bParamExists = defaults.doesParameterDefaultExist(key.toString());

                    if (!bParamExists) {

                        key = "[NoDef] " + key;
                    } else {

                        Object def = defaults.getParameter(key);

                        if (def != null && value != null) {

                            if (!BEncoder.objectsAreIdentical(def, value)) {

                                key = "-> " + key;
                            }
                        }
                    }

                    if (value instanceof Long) {

                        writer.println(key + "=" + value);

                    } else if (value instanceof List) {

                        writer.println(key + "=" + BDecoder.decodeStrings((List) BEncoder.clone(value)) + "[list]");

                    } else if (value instanceof Map) {

                        writer.println(key + "=" + BDecoder.decodeStrings((Map) BEncoder.clone(value)) + "[map]");

                    } else if (value instanceof byte[]) {

                        byte[] b = (byte[]) value;

                        boolean hex = false;

                        for (int i = 0; i < b.length; i++) {

                            char c = (char) b[i];

                            if (!(Character.isLetterOrDigit(c) || "\\ `¬\"£$%^&*()-_=+[{]};:'@#~,<.>/?'".indexOf(c) != -1)) {

                                hex = true;

                                break;
                            }
                        }
                        writer.println(key + "=" + (hex ? ByteFormatter.nicePrint(b) : bytesToString((byte[]) value)));

                    } else {

                        writer.println(key + "=" + value + "[unknown]");
                    }
                }
            } finally {

                writer.exdent();
            }
        } finally {

            writer.exdent();
        }
    }

    public void dumpConfigChanges(IndentWriter writer) {
        ConfigurationDefaults defaults = ConfigurationDefaults.getInstance();

        Set<String> keys = new TreeSet<String>(new Comparator<String>() {
            public int compare(String o1, String o2) {
                return (o1.compareToIgnoreCase(o2));
            }
        });

        keys.addAll(propertiesMap.keySet());

        Iterator<String> it = keys.iterator();

        while (it.hasNext()) {

            String key = it.next();

            // don't dump crypto stuff

            if (ignoreKeyForDump(key)) {

                continue;
            }

            Object value = propertiesMap.get(key);

            boolean bParamExists = defaults.doesParameterDefaultExist(key.toString());

            if (bParamExists) {

                Object def = defaults.getParameter(key);

                if (def != null && value != null) {

                    if (!BEncoder.objectsAreIdentical(def, value)) {

                        if (value instanceof Long) {

                            writer.println(key + "=" + value);

                        } else if (value instanceof List) {

                            writer.println(key + "=" + BDecoder.decodeStrings((List) BEncoder.clone(value)) + "[list]");

                        } else if (value instanceof Map) {

                            writer.println(key + "=" + BDecoder.decodeStrings((Map) BEncoder.clone(value)) + "[map]");

                        } else if (value instanceof byte[]) {

                            byte[] b = (byte[]) value;

                            boolean hex = false;

                            for (int i = 0; i < b.length; i++) {

                                char c = (char) b[i];

                                if (!(Character.isLetterOrDigit(c) || "\\ `¬\"£$%^&*()-_=+[{]};:'@#~,<.>/?'".indexOf(c) != -1)) {

                                    hex = true;

                                    break;
                                }
                            }
                            writer.println(key + "=" + (hex ? ByteFormatter.nicePrint(b) : bytesToString((byte[]) value)));

                        } else {

                            writer.println(key + "=" + value + "[unknown]");
                        }
                    }
                }
            }
        }
    }

    protected static String bytesToString(byte[] bytes) {
        try {
            return (new String(bytes, Constants.DEFAULT_ENCODING));

        } catch (Throwable e) {

            return (new String(bytes));
        }
    }

    protected static byte[] stringToBytes(String str) {
        if (str == null) {

            return (null);
        }

        try {
            return (str.getBytes(Constants.DEFAULT_ENCODING));

        } catch (Throwable e) {

            return (str.getBytes());
        }
    }

}